完成昨天的演示後,也許有人會覺得,處理落葉動畫的流程很簡單,就是「讓落葉自然落下」然後「在畫布上繪製落葉」兩步驟而已,然而實作上總是比較複雜,可能還會有「被風吹起」、「被車撞到」、「被蜘蛛絲捕獲」等等的不同狀態,也有可能是同時發生,概念上像是這樣:
隨著功能越來越豐富,我們會逐漸開始遇到程式碼維護上的困難,還有一個狀況是,如果我們要做一個暫停效果呢?大概很直覺就會寫成這樣:
function MouseAnime(){
if(paused == false){
// ......
// 一大堆程式碼
// ......
}
else{
// 在畫布上繪製不動的落葉
}
}
這樣寫最大的問題是,中間程式碼越來越多時,逐漸閱讀困難,為了讓它一目了然,就會需要包成幾個函式/方法:
function 自然落下方法(){ ...... }
function 被風吹起方法(){ ...... }
function 被車撞到方法(){ ...... }
function 被蜘蛛網捕獲方法(){ ...... }
fucntion MouseAnime(){
if(paused == false){
自然落下方法();
被風吹起方法();
被車撞到方法();
被蜘蛛網捕獲方法();
}
else{
// 在畫布上繪製不動的落葉
}
}
現在看起來乾淨多了,但另一個問題開始出現,所有的變數(包含以上function)都在幾乎都在最外層,乍看之下命名還算清楚,但是這些變數都暴露在外,這樣的潛台詞就相當於說,這些方法都是公開的,沒有限制誰都可以使用,此時若有數十個上百個變數都在同一個範疇下,很快就會在讀程式碼時開始困惑「被車撞到方法是給誰用的?」或「這個那個是幹嘛的」,必須Ctrl+F來回比對才知道這些變數是設計給誰用的,因為「不知道這些變數」屬於誰。
要解決這個問題,首先我們要先理解一個蘋果的本質,為此,JS引入了一個概念,稱之為「物件」,可以看到,一個紅色飽滿的富士蘋果長這個樣子:
let Apple = {
'variety': 'Fuji',
'color': 'red',
'taste': 'juicy'
}
console.log(Apple.variety); // 'Fuji'
console.log(Apple.color); // 'red'
console.log(Apple.taste); // 'juicy'
反過來講,也可以後天塑造而成:
let fakeApple = {};
fakeApple.variety = 'Fuji';
fakeApple.color = 'red';
fakeApple.taste = 'juicy';
console.log(fakeApple.variety); // 'Fuji'
console.log(fakeApple.color); // 'red'
console.log(fakeApple.taste); // 'juicy'
像是這個假貨,看起來很好吃,實際上只是偽裝成富士蘋果
讓我們檢查一下剛剛的蘋果是否受地心引力制約:
let Apple = {
'variety': 'Fuji',
'color': 'red',
'taste': 'juicy',
'gravity': 9.8,
'velocity': 0,
'height': 100,
'fall': function(){
this.velocity+= this.gravity*0.016;
this.height-= this.velocity;
console.log(this.height);
},
'manner': 'bad'
}
for(let N=0; N<40; N++){
Apple.fall();
}
console.log(Apple.manner) // bad
剛剛提到value可以是任何型別,也包括了函式!因此我們可以用Apple.fall呼叫它,是Apple專用的函式呢!還可以透過this呼叫自己來取得自己的其他屬性。
格式相當於常見的函式命名法let fall = function(){......}
原來,地心引力確實存在,至於...有沒有掉到牛頓頭上呢?這顆蘋果這麼沒禮貌,也是有可能故意跑去砸牛頓,然後在他頭上爆開,我只能說:不排除這個可能性!
確實,我們可以立刻開始著手修改我們昨天設計的落葉,改成像上面蘋果的格式一樣,然而,這邊會一個問題,如果我今天要用兩片葉子怎麼辦,總不會同樣的格式再寫第二遍吧?然後要N片就跑N遍迴圈...唉呀!用想的就累,其實,我們還缺少一個重要的步驟,就是替它設計一個建構式(Constructor)
概念上就是,我們可以設計一個物件產生器,然後需要落葉的時候,就用產生器創造一個出來。咦?鳩豆麻蝶,這段敘述有沒有覺得有點熟悉呢?是不是跟這句話很像呢:「我們可以設計一個陣列產生器,然後需要陣列的時候,就用產生器創造一個出來」,寫成代碼如下:
let myGirlfriend = new Array(10);
let myMoney = new Number(1000000);
這樣的寫法是不是很熟悉呢?其實我們之所以能使用陣列的各種方法諸如splice、reduce、forEach,便是因為有這個稱之為「Array」的建構式,它把陣列常用的方法全都定義好了,因此myGirlfriend就會有很多方法可以用,像是
三人行剛剛的Apple有fall這個方法可以用一樣。
那麼,建構式要怎麼寫呢?最簡易的形式如下,當中的this所指涉的對象,是當你使用建構式時,它會回傳的對象return this;
,那麼我們就是從一開始設計蘋果時寫apple.key=value
,改寫成this.key=value
,就能成為一個模板,比如,我們拿昨天的落葉動畫來修改,可以這樣寫:
function leafMaker(){
this.timestamp = Date.now();
this.lifeCycle = 6;
//......
// 省略(寬高、起始點、角速度等等昨天寫的所有參數)
//......
this.fall = function(context ,timestamp){
let deltaTS = (timestamp - this.timestamp) / 1000;
if(this.lifeCycle > deltaTS){
let rotateNow = this.rotateTheta + this.rotateOmega * deltaTS;
let revolveNow = this.revolveTheta + this.revolveOmega * deltaTS;
let cursorX = this.originX + 500 * Math.sin(revolveNow);
let cursorY = this.originY + 200 * Math.sin(revolveNow)
+ 100 * deltaTS;
context.save();
context.translate(cursorX, cursorY);
context.rotate(rotateNow);
context.drawImage(leafImg, -this.width/2, -this.height/2, this.width, this.height);
context.restore();
}
}
// 這邊JS省略了return this的寫法
}
還有一個小重點是,這個leafMaker只是一個建構式,並沒有leafMaker.fall這樣的方法,就像平常都是用new Array建構式來建立陣列資料,那麼你就不會期待可以用Array.splice一樣。
有了建構式後,我們可以在滑鼠點擊的當下,產生一個落葉物件,並賦值給名為leaf的變數:
let leaf;
canvas.addEventListener('click', SetMouse);
function SetMouse(e){
leaf = new leafMaker();
}
原本初始化落葉的代碼都塞在SetMouse裡面,現在變得很乾淨
並且在每一偵的動畫循環,只需要這樣寫:
function MouseAnime(){
Clear(context);
leaf.fall(context);
}
原本計算落葉的方程式都塞在MouseAnime裡面,現在變得很乾淨
說到這,我想大家應該稍微明白了物件的魅力在哪裡了吧?原本落葉的相關程式碼四散各處:
這時候,若想要實現一開始的流程圖繪製的各種落葉效果(被風吹起等等),是不是方便許多了呢?
還記得一開始談到屬於誰的概念嗎?在人類世界,看待與理解每一件事情,都會一層又一層,並且和其他類似的產生關連,想到蘋果,就會聯想到一些屬性,像是「長在樹上」、「會掉到地上」、「表面有一層蠟」,接著又會聯想到香蕉也長在樹上、葡萄、藍莓表皮也有果蠟,也因其錯綜複雜的關係,像是生物界就被歸類為了「界門綱目科屬種」。
要如何實現一環扣一環和共用相同的屬性,便是物件誕生之初的使命和意義,接下來幾篇我們將會前進到更深入的環節,試想,所有在地球上的物體都會受到地心引力的影響,也就是說,這是一個共用的屬性,因此,我們不需要把重力公式寫在Apple、Banana、Lemon每個物件的裡面,能達成這一目的概念就叫做繼承。
什麼是繼承呢?請期待本章節後續的文章!